/*
	J-Bird net/sourceforge/jbird/io/DelimitedTokenizer.java

        Copyright (C) 2001-2007 Dick Repasky
	drepasky@users.sourceforge.net

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License 
	as published by the Free Software Foundation; either version 2
	of the License, or (at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
	General Public License for more details.

	You should have received a copy of the GNU General Public 
	License along with this program; if not, write to the Free Software 
	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
*/
package net.sourceforge.jbird.io;

import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;

/** A tokenizer for delimited data.  Delimiters are limited to
  * a single character (',' by default).  Any value containing
  * the delimiter must be quoted.  By default, either single
  * or double quotes may be used.  Once one type of quote is
  * used to begin a quoted string, the other type may be used
  * freely inside.  In all other cases in which a quote is desired
  * as a character in a string, it should be escaped by using two
  * quotes in successing or by preceeding the quote
  * with a backslash.  Backslashes serve as escape characters
  * only in front of quotes that are inside of quoted fields.
  * <p>
  * This class handles CSV output from Microsoft Excel: fields
  * containing delimiters or quote characters are quoted and 
  * quotes within quoted fields are escaped using two successive quotes.
  * <p>
  * Ends of lines discovered within quoted strings terminate the
  * quotes as well as the lines and are not treated as errors.
  * <p>
  * Works on UNIX, Mac and DOS files.
  * <p>
  * Note that the capacity of the internal ArrayList that is used
  * to accumulate fields is the larges number of fields encountered
  * on a line so far.
  * @author Dick Repasky
*/

public class DelimitedTokenizer {

   protected Reader reader;
   protected StringBuffer stringbuf = new StringBuffer(100);
   protected ArrayList arraylist = new ArrayList();
   protected int   arraycapacity;
   protected int[] quotechars;
   protected int   currentquote;
   protected int   quoteescape = '\\';
   protected int   delim = ',';
   protected int state = 0;		// 0 = relaying characters
					//     ready for start of
					//     quote
					// 1 =  relaying characters
					//      of quoted string
					// 2 =  found \r

   public DelimitedTokenizer(Reader r) {
	reader = r;
	stringbuf.ensureCapacity(100);
	quotechars = new int[2];
	quotechars[0] = '"';
	quotechars[1] = '\'';
   }

   public void close() throws IOException {
	if (reader != null) {
	   reader.close();
	}
	reader = null;
   }

   public void setQuoteChars(int[] chara) {
	quotechars = chara;
   }

   public void setEscapeChar(int achar) {
	quoteescape = achar;
   }

   public void setFieldDelimiter(int achar) {
	delim = achar;
   }

   /**
     * Read the next line, returning values on it as an
     * array of Strings.
     * <p>
     * Not very efficient if you have a loop in which you
     * force the type casts yourself from Object[]
     * returned by getLineObjects().
     *
   */
     
   public String[] getLineStrings () throws IOException {
	Object[] oba = getLineObjects();
	String[] answer = null;
	if (oba != null) {
	   int cap = oba.length;
	   answer = new String[cap];
	   int i;
	   for (i = 0; i < cap; i++ ) {
		answer[i] = (String)oba[i];
		oba[i] = null;
	   }
	   oba = null;
	}
	return answer;
   }

   /**
     * Read the next line, returning values on it as an
     * array of Objects.  All objects in the array are strings.
     *
     * The only quotes that are honored are those that occur
     * as the first character in a field and as the final.
     * Examples using ',' as delimiter:
     *
     *  INPUT				Single field that results<br>
     *
     * ,"hi ther",			hi there<br>
     * ,"hi there, fellas",		hi there, fellas<br>
     * ,"""hi ther""",			"hi there"<br>
     * ,hi "there",			hi "there"<br>
     * ,"hi "there",			hi "there<br>
     * ,"hi "there, other, stuff\n   	hi "there, other, stuff<br>
   */

   public Object[] getLineObjects () throws IOException {
					// empty array list
	arraylist.clear();
	emptyStringBuf();
	int prevpos = -1;		// location of last char in
					// buffer
	if (state != 2) {
		state = 0;
	}
	int prevchar = '0';		// anything but \ will do
					// keep count of contiguous quotes
					// when state = 1
	int quotecount = 0;
        int achar = reader.read();
        while (achar > -1) {
	   if (state == 2) {		// previous char was \r
		if (achar != '\n') {
		   state = 0;
		}
	   }
	   if (state == 0) {			// outside of quotes
	   	if (achar == delim || achar == '\r' || achar == '\n') {
			arraylist.add(stringbuf.toString());
			emptyStringBuf();
			prevpos = -1;
			if (achar != delim) {
				if (achar == '\r') {
				   state = 2;
				}
				checkArrayCapacity();
				return arraylist.toArray();
			}
		} else if (isQuote(achar) && prevpos == -1) {
			   currentquote = achar;
			   achar = 0; // remove meaning for next char
			   quotecount = 0; // prep for state = 1
			   state = 1;
		} else {
			stringbuf.append((char)achar);
			prevpos ++;
		}
		
	   } else if (state == 1) {	// inside quotes
		if (quotecount == 2) {	// two successive quotes escape
			stringbuf.deleteCharAt(prevpos);
			prevpos --;
			prevchar = 0;
			quotecount = 0;
		}
		if (achar == delim && prevchar == currentquote) {
			stringbuf.deleteCharAt(prevpos);
			arraylist.add(stringbuf.toString());
			emptyStringBuf();
			prevpos = -1;
			state = 0;
		} else if (achar == '\r' || achar == '\n') {
			if (quotecount == 1) {
				stringbuf.deleteCharAt(prevpos);
			}
			arraylist.add(stringbuf.toString());
			emptyStringBuf();
			if (achar == '\r') {
			   state = 2;
			}
			checkArrayCapacity();
			return arraylist.toArray();
		} else if (achar == currentquote && prevchar == quoteescape) {
				// Current escaped by previous \.
				// Delete previous, add current
			stringbuf.deleteCharAt(prevpos);
			stringbuf.append((char)achar);
			achar = 0; // detach meaning for next char
			quotecount = 0; // implies "\" will be interpreted as ""
		} else {
			if (achar == currentquote) {
				quotecount ++;
			} else {
				quotecount = 0;
			}
			stringbuf.append((char)achar);
			prevpos ++;
		}
	   } else {		// state == 2 && achar == '\n'
		state = 0;
	   }
	   prevchar = achar;
	   achar = reader.read();
       } 			// EOF found
       if (prevpos > -1) {
	  if (state == 1 && prevchar == currentquote) {	// quoted field
							// delete endquote
		stringbuf.deleteCharAt(stringbuf.length() - 1);
	  }
	  arraylist.add(stringbuf.toString());
       }
       Object[] answer = null;
       if (! arraylist.isEmpty()) {
	   answer = arraylist.toArray();
       }
       return answer;
   }

   private final void emptyStringBuf() {
	int rm = stringbuf.length();
	if (rm >= 0) {
		stringbuf.delete(0, rm);
	}
   }

   private final boolean isQuote(int achar) {
	int max = quotechars.length;
	int i;
	for (i=0; i < max; i++) {
		if (achar == quotechars[i]) {
			return true;
		}
	}
	return false;
   }

   private final void checkArrayCapacity() {
	int size = arraylist.size();
	if (size > arraycapacity) {
		arraylist.ensureCapacity(size);
		arraycapacity = size;
	}
   }

}
